package com.twoscreen;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Random;
import java.util.logging.Logger;

import javax.jdo.JDOObjectNotFoundException;
import javax.jdo.PersistenceManager;
import javax.jdo.Query;
import javax.jdo.annotations.IdGeneratorStrategy;
import javax.jdo.annotations.NotPersistent;
import javax.jdo.annotations.PersistenceCapable;
import javax.jdo.annotations.Persistent;
import javax.jdo.annotations.PrimaryKey;

import org.datanucleus.exceptions.NoPersistenceInformationException;
import org.json.JSONObject;

import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;
import com.google.appengine.api.users.User;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.twoscreen.test.Test;
import com.twoscreen.test.TestMethod;

@PersistenceCapable
public class TUser {
	@NotPersistent
	private static PersistenceManager pm = PMF.get().getPersistenceManager();
	
	@NotPersistent
	private static final Logger log = Logger.getLogger("com.twoscreen.TUser.java");
	
	@NotPersistent
	private static final String clientID = "859612904977.apps.googleusercontent.com";
	
	@NotPersistent
	private static final String developer_key = "VXkRXuV8DekjqsOEeFVGRsAT";
	
	@NotPersistent
	private static final String callbackURI = "https://two-screen.appspot.com/oauth2callback";
	
	@NotPersistent
	private static final String viewerIdPropertyName = "viewerId";
	
	@NotPersistent
	private static final String userNamePropertyName = "userName";

	@NotPersistent
	private static final String userPropertyName = "user";
	
	@NotPersistent
	private static final String producerAccessPropertyName = "producerAccessToken";
	
	@NotPersistent
	private static final String producerAccessExpiryPropertyName = "producerAccessTokenExpiry";
	
	@NotPersistent
	private static final String producerRefreshPropertyName = "producerRefreshToken";
	
	@PrimaryKey
	@Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
	private Key key;
	
	@Persistent
	private String viewerId;
	
	@Persistent
	private String userName;
	
	@Persistent
	private User user;
	
	@Persistent
	private String producerAccessToken;
	
	@Persistent
	private Date producerAccessTokenExpiry;
	
	@Persistent
	private String producerRefreshToken;
	
	private TUser(String viewerId, User user) {
		Key key = KeyFactory.createKey(TUser.class.getSimpleName(), user.getUserId());
		this.key = key;
		this.viewerId = viewerId;
		this.user = user;
		pm.makePersistent(this);
		log.info("Twoscreen User added: viewerId: " + viewerId);
	}
	
	public Key getKey() {
		return key;
	}

	public String getViewerId() {
		return viewerId;
	}
	
	public String getUserName() {
		return userName;
	}
	
	private void setUserName(String userName) {
		this.userName = userName;
	}

	public User getUser() {
		return user;
	}
	
	public String getProducerAccessToken() {
		return producerAccessToken;
	}

	public void setProducerAccessToken(String producerAccessToken) {
		this.producerAccessToken = producerAccessToken;
	}
	
	public Date getProducerAccessExpiry() {
		return producerAccessTokenExpiry;
	}

	public void setProducerAccessExpiry(Date producerAccessTokenExpiry) {
		this.producerAccessTokenExpiry = producerAccessTokenExpiry;
	}
	
	public String getProducerRefreshToken() {
		return producerRefreshToken;
	}

	public void setProducerRefreshToken(String producerRefreshToken) {
		this.producerRefreshToken = producerRefreshToken;
	}

	public static String getUserpropertyname() {
		return userPropertyName;
	}

	public static String getVieweridpropertyname() {
		return viewerIdPropertyName;
	}

	/**
	 * Returns a TUser object for a given Google User object
	 * @param user Google user object
	 * @return TUser object
	 */
	public static TUser getTUser(User user) {
		log.info("Getting user id: " + user.getEmail());
		TUser tuser = null;
		pm.getObjectIdClass(TUser.class);
		try {
			//found solution here: http://db.apache.org/jdo/api20/apidocs/javax/jdo/PersistenceManager.html
			Key key = KeyFactory.createKey(TUser.class.getSimpleName(), user.getUserId());
			log.info("key: " + key);
			tuser = (TUser) pm.getObjectById(TUser.class, key);
		} catch (NullPointerException e) {
			log.info("Test failed on userID: " + user.getUserId() + e);
		} catch (JDOObjectNotFoundException e) {
			log.info("Test failed on userID: " + user.getUserId() + e);
		} catch (NoPersistenceInformationException e) {
			log.warning("No data in DataStore User ID: " + user.getUserId() + e);
		}
		
		if (tuser == null) {
			log.info("Creating a new viewerId");
			String viewerId = getId(7); //allows for 24,893,071,176,241,500,000,000,000,000,000,000,000,000,000,000,000,000 IDs
			tuser = new TUser(viewerId, user);
			log.info("Created and stored viewerId: " + viewerId + " successfully");
		}
		
		log.info("returning tuser");
		return tuser;
	}
	
	/**
	 * Searches the data store for a TUser object based on the passed viewer ID
	 * @param viewerId The viewer ID to search for
	 * @return TUser object
	 */
	public static TUser getTUserByViewerId(String viewerId) {
		log.info("Getting user");
		TUser result = null;
		Query q = pm.newQuery(TUser.class);
		q.setFilter(viewerIdPropertyName + " == viewerIdParam");
		q.declareParameters("String viewerIdParam");
		try {
			log.info("Running query");
			@SuppressWarnings("unchecked")
			List<TUser> results = (List<TUser>) q.execute(viewerId);
			if (results.size() > 1) {
				log.warning("Found " + results.size() + " users with viewerID: " + viewerId);
			} else if (results.size() != 0) {
				result = results.get(0);
			} else {
				log.info("no users found for viewerId: " + viewerId);
			}
		} finally {
			q.closeAll();
		}
		return result;
	}
	
	/**
	 * Finds the passed viewer ID in the data store and update the YouTube access token,
	 * refresh token, and expiry date
	 * @param access Access token received from YouTube
	 * @param expires Expiry date received from YouTube
	 * @param refresh Refresh token received from YouTube
	 * @param viewerId Viewer ID to find and save to
	 */
	protected static void addYtTokens(String access, Date expires, String refresh, String viewerId) {
		//Get the user and save data
		PersistenceManager pm = PMF.get().getPersistenceManager();
		Query q = pm.newQuery(TUser.class);
		q.setFilter(viewerIdPropertyName + " == viewerIdParam");
		q.declareParameters("String viewerIdParam");
		try {
			log.info("Running query");
			@SuppressWarnings("unchecked")
			List<TUser> results = (List<TUser>) q.execute(viewerId);
			if (results.size() > 1) {
				log.warning("Found " + results.size() + " users with viewerID: " + viewerId);
			} else if (results.size() != 0) {
				TUser tuser = results.get(0);
				if (access != null) {
					log.info("saving the access token");
					tuser.setProducerAccessToken(access);
				}
				if (expires != null) {
					log.info("saving the expiry");
					tuser.setProducerAccessExpiry(expires);
				}
				if (refresh != null) {
					log.info("saving the refresh token");
					tuser.setProducerRefreshToken(refresh);
				}
			} else {
				log.info("no users found for viewerId: " + viewerId);
			}
		} finally {
			q.closeAll();
			pm.close();
		}
	}

	/**
	 * Attempts to find a TUser in the data store for a given Google User object
	 * If no TUser is found a new one is created
	 * Either case the viewer ID is returned
	 * @param user The Google User object to search for
	 * @return A String containing the viewer ID
	 */
	public static String getViewerId(User user) {
		//Check to see if the user has a viewer ID
		String viewerId = null;
		log.info("Attempting to find the viewer in the datastore to assign their viewerId");
		try {
			Key key = KeyFactory.createKey(TUser.class.getSimpleName(), user.getUserId());
			//found solution here: http://db.apache.org/jdo/api20/apidocs/javax/jdo/PersistenceManager.html
			viewerId = ((TUser) pm.getObjectById(TUser.class, key)).getViewerId(); 
			log.info("Found viewerId: " + viewerId);
		} catch (NullPointerException e) {
			log.info("Test failed " + e);
		} catch (JDOObjectNotFoundException e) {
			log.info("Test failed " + e);
		} catch (NoPersistenceInformationException e) {
			log.warning("No data in DataStore " + e);
		}
		
		//Create a new viewerId and store it if one does not exist
		if (viewerId == null) {
			log.info("Creating a new viewerId");
			viewerId = getId(7); //allows for 24,893,071,176,241,500,000,000,000,000,000,000,000,000,000,000,000,000 IDs
			new TUser(viewerId, user);
			log.info("Created and stored viewerId: " + viewerId + " successfully");
		}
		
		return viewerId;
	}
	
	/**
	 * Contacts YouTube servers to obtain a new access token and expiry for the
	 * passed Google User object
	 * @param user Google User object to refresh
	 * @return New access token for YouTube
	 */
	public static String refreshAccessToken(User user) {
		log.info("Checking access tokens");
		String refreshedToken = null;
		String tokens = null;
		
		//Get the user
		PersistenceManager pm = PMF.get().getPersistenceManager();
		Key key = KeyFactory.createKey(TUser.class.getSimpleName(), user.getUserId());
		TUser tuser = pm.getObjectById(TUser.class, key);
		log.info("Got user: " + tuser.getViewerId());
		
		//Attempt to get a new access token
		
		//Send post message to get new tokens
		log.info("Refreshing access token");
		String tokenRequestURI = "https://accounts.google.com/o/oauth2/token";
		String tokenParameters = "client_id=" + clientID +
				"&client_secret=" + developer_key +
				"&refresh_token=" + tuser.getProducerRefreshToken() +
				"&grant_type=refresh_token";
	    try {
	        URL url = new URL(tokenRequestURI);
	        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
	        connection.setDoOutput(true);
	        connection.setRequestMethod("POST");
	        connection.addRequestProperty("Content-Type", "application/x-www-form-urlencoded");

	        OutputStreamWriter writer = new OutputStreamWriter(connection.getOutputStream());
	        log.info("message to be written: " + tokenParameters);
	        writer.write(tokenParameters);
	        writer.close();

	        if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {
	        	log.info("Connection ok");
	        } else {
	            // Server returned HTTP error code.
	        	log.info("error" + connection.getResponseCode());
	        }
	        StringBuilder sb = new StringBuilder();
			BufferedReader reader = null;
		    try {
		        reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
		        for (String line; (line = reader.readLine()) != null;) {
		            sb.append(line);
		            log.info("adding: " + line);
		        }
		    } finally {
		        if (reader != null) try { reader.close(); } catch (IOException logOrIgnore) {}
		    }
		    tokens = sb.toString();
	    } catch (MalformedURLException e) {
	    	log.warning("Malformed URL in Auth token request: " + e);
	    } catch (IOException e) {
	    	log.warning("IO exception in Auth token request: " + e);
	    }
	    
	    //Get the codes from the received JSON
		log.info("Parsing JSON : " + tokens);
		JsonParser parser = new JsonParser();
		JsonObject obj = parser.parse(tokens).getAsJsonObject();
		log.info("JSON object is: " + obj);
		String newAccess = null;
		int newExpires = 0;
		try {
			newAccess = obj.get("access_token").getAsString();
			newExpires = obj.get("expires_in").getAsInt();
		} catch (NullPointerException e) {}
		
		log.info("new access token: " + newAccess + " new expiry: " + newExpires);

		//Get the user and save to data store and return to requester
		if (newAccess != null) {
			log.info("saving the new access token");
			tuser.setProducerAccessToken(newAccess);
			refreshedToken = newAccess;
		}
		if (newExpires != 0) {
			log.info("saving the new expiry");
			Date d = new Date();
			long expiryTime = d.getTime() + (newExpires * 1000);
			log.info("current time: " + d.getTime() + " setting expiry to: " + expiryTime);
			tuser.setProducerAccessExpiry(new Date(expiryTime));
		}
		
		//Close and persist
		pm.close();
		
		return refreshedToken;
	}
	
	/**
	 * Searches the data store for passed Google User object and updates the 
	 * TUser user name
	 * @param user Google User object
	 * @param userName User name to change to
	 */
	public static void saveUserName(User user, String userName) {
		log.info("Getting user id: " + user.getEmail());
		
		//Get the TUser
		TUser tuser = null;
		PersistenceManager pm = PMF.get().getPersistenceManager();
		pm.getObjectIdClass(TUser.class);
		try {
			Key key = KeyFactory.createKey(TUser.class.getSimpleName(), user.getUserId());
			tuser = (TUser) pm.getObjectById(TUser.class, key);
		} catch (NullPointerException e) {
			log.info("Test failed on userID: " + user.getUserId() + e);
		} catch (JDOObjectNotFoundException e) {
			log.info("Test failed on userID: " + user.getUserId() + e);
		} catch (NoPersistenceInformationException e) {
			log.warning("No data in DataStore User ID: " + user.getUserId() + e);
		}
		
		//Set the username
		if (tuser != null) {
			tuser.setUserName(userName);
			log.info("Username saved");
		} else {
			log.warning("User not found");
		}
		pm.close();
	}
	
	/**
	 * Checks the passed username for uniqueness
	 * @param userName User name to check
	 * @return True if valid
	 */
	public static Boolean checkUserName(String userName) {
		log.info("Checking user name: " + userName);
		Boolean valid = true;
		
		//Query a list of user names
		Query q = pm.newQuery(TUser.class);
		q.setFilter(userNamePropertyName + " != null");
		@SuppressWarnings("unchecked")
		List<TUser> results = (List<TUser>) q.execute();
		q.closeAll();
		
		//Check the user name is not in the list
		if (results.size() > 0) {
			log.info("Found " + results.size() + " user names");
			for (TUser result : results) {
				if (result.getUserName().equals(userName)) {
					valid = false;
					log.info("Username exists");
				}
			}
		} else {
			log.warning("No usernames found");
		}
		
		return valid;
	}
	
	/**
	 * Creates an array of 3 user name suggestions for a passed user name
	 * The method assumes the user name is not unique
	 * @param userName The user name to generate suggestions for
	 * @return An array of Strings containing user name suggestions
	 */
	public static String[] getUserNameSuggestions(String userName) {
		log.info("Creating name suggestions");
		String[] suggestions = new String[3];
		log.info("Current suggestions: " + suggestions.length);
		String[] additions = {
			"funny", "monkey", "daddy", "norris", "kick", "dude", "lovely", "trend", "back", "kitty",
			"lover", "potato", "sweet", "black", "blue", "orange", "red", "green", "purple", "silver",
			"gold", "dark", "light", "white"
		};
		log.info("Additions length: " + additions.length);
		//Ideas from: http://www.squidoo.com/cool-usernames : 
		
		//Query a list of user names
		log.info("Quering usernames");
		Query q = pm.newQuery(TUser.class);
		q.setFilter(userNamePropertyName + " != null");
		@SuppressWarnings("unchecked")
		List<TUser> results = (List<TUser>) q.execute();
		q.closeAll();
		log.info("Found " + results.size() + " user names");
		
		//Create a new user name and check it until the suggestion array is full
		int suggestionCount = 0;
		ArrayList<Integer> usedRndNo = new ArrayList<Integer>();
		do {
			String name = null;
			Boolean tooLong = true;
			//Get a random name and side
			Random generator = new Random();
			Boolean test = false;
			int randNameNo = 0;
			do {
				randNameNo = generator.nextInt(additions.length);
				test = usedRndNo.contains(randNameNo);
				if (test == false) {
					usedRndNo.add(randNameNo);
				}
			} while (test == true);
			int randNameSide = generator.nextInt(2);
			log.info("Random name no: " + randNameNo + " random side no: " + randNameSide);
			
			if (tooLong) {
				userName.substring(0, userName.length()/2);
			}
			if (randNameSide == 1) {
				name = userName + additions[randNameNo];
			} else {
				name = additions[randNameNo] + userName;
			}
			log.info("User name to test: " + name);
			
			//if its valid add to the array
			Boolean valid = true;
			
			//Check the user name is not in the list and less than 30 characters
			if (results.size() > 0) {
				for (TUser result : results) {
					if (result.getUserName().equals(name)) {
						valid = false;
					}
				}
			} else {
				log.warning("No usernames found");
			}
			if (name.length() > 30) {
				valid = false;
				tooLong = true;
			}
			
			if (valid) {
				suggestions[suggestionCount] = name;
				suggestionCount++;
			}
		} while (suggestionCount < 3);
		
		
		return suggestions;
	}
	
	/**
	 * Generates a unique ID containing upper and lower case characters and numbers
	 * @param length The length of ID to be created
	 * @return A unique ID
	 */
	private static String getId(int length) {
		Boolean validId = false;
		String ID;
		
		do {
			ID = CodeGenerator.createID(length);
			log.info("Start exists test for: " + ID);
			boolean ex = exists(ID);
			log.info("Exists test result: " + ex + " for ID: " + ID);
			if (!ex) {
				validId = true;
			}
		} while (validId == false);
		log.info("returning ID");
		return ID;
	}
	
	/**
	 * Checks the data store and returns a boolean
	 * @param ID The ID to check in the data store
	 * @return True if exists in data store
	 */
	private static boolean exists(String ID) {
		boolean rtn = false;
		Query q = null;
		try {
			q = pm.newQuery(TUser.class);
			@SuppressWarnings("unchecked")
			List<TUser> results = (List<TUser>) q.execute();
			for (TUser result : results) {
				String viewerId = result.getViewerId();
				log.info("Checking viewer ID: " + viewerId);
				
				if (viewerId.equals(ID)) {
					log.info("Viewer ID " + viewerId + " is in the data store");
					rtn = true;
				}
			}
		} catch (NullPointerException e) {
			log.info("Query returned null: " + e);
		} finally {
			q.closeAll();
		}
		
		return rtn;
	}
	
	/**
	 * Runs all the tests on the class
	 * @return Test results JSON
	 */
	public static JSONObject test() {
		Test test = new Test("TUser");
		test.addMethod(testConstructor());
		test.addMethod(testAddYtTokens());
		test.addMethod(testSaveUserName());
		test.addMethod(testCheckUserName());
		test.addMethod(testGetUserNameSuggestions());
		return test.getJSONObject();
	}
	
	/**
	 * Tests the constructor method
	 * @return Test results JSON
	 */
	private static JSONObject testConstructor() {
		TestMethod tm = new TestMethod("testConstructor");
		
		//Test base
		String viewerId = "abcd";
		String email = "efgh";
		String authDomain = "ijkl";
		String userId = "mnop";
		User user = new User(email, authDomain, userId);
		TUser test = getTUserByViewerId(viewerId);
    	
		//Test 1
		Boolean test1 = false;
    	String testStack1 = "";
    	try {
    		TUser inTest = pm.getObjectById(TUser.class, user.getUserId());
    		test1 = (inTest != null);
    	} catch (Exception e) {
    		testStack1 = e.getMessage();
    	}
		tm.addTest("Entry exists", test1, testStack1);
		
		//Remove the test object
		pm.deletePersistent(test);
		
    	return tm.getJSONObject();
    }
	
	/**
	 * Tests the addYtTokens method
	 * @return Test results JSON
	 */
	private static JSONObject testAddYtTokens() {
		TestMethod tm = new TestMethod("testAddYtTokens");
		
		//Test base
		String viewerId = "efgh";
		TUser test = getTUserByViewerId(viewerId);
		String access = "abcd";
    	Date expires = new Date();
    	String refresh = "ijkl";
    	addYtTokens(access, expires, refresh, viewerId);
    	
		//Test 1
		Boolean test1 = false;
    	String testStack1 = "";
    	try {
    		test1 = (test.getProducerAccessToken().equals(access));
    	} catch (Exception e) {
    		testStack1 = e.getMessage();
    	}
		tm.addTest("Access token correct", test1, testStack1);
		
		//Test 2
		Boolean test2 = false;
    	String testStack2 = "";
    	try {
    		test2 = (test.getProducerAccessExpiry().equals(expires));
    	} catch (Exception e) {
    		testStack2 = e.getMessage();
    	}
		tm.addTest("Expiry date correct", test2, testStack2);
		
		//Test 3
		Boolean test3 = false;
    	String testStack3 = "";
    	try {
    		test3 = (test.getProducerRefreshToken().equals(refresh));
    	} catch (Exception e) {
    		testStack3 = e.getMessage();
    	}
		tm.addTest("Refresh token correct", test3, testStack3);
		
		//Remove the test object
		pm.deletePersistent(test);
		
    	return tm.getJSONObject();
    }
	
	/**
	 * Tests the saveUserName method
	 * @return Test results JSON
	 */
	private static JSONObject testSaveUserName() {
		TestMethod tm = new TestMethod("testSaveUserName");
		
		//Test base
		String viewerId = "abcd";
		String email = "efgh";
		String authDomain = "ijkl";
		String userId = "mnop";
		String userName = "qrst";
		User user = new User(email, authDomain, userId);
		TUser test = getTUserByViewerId(viewerId);
		saveUserName(user, userName);
    	
		//Test 1
		Boolean test1 = false;
    	String testStack1 = "";
    	try {
    		test1 = (test.getUserName().equals(userName));
    	} catch (Exception e) {
    		testStack1 = e.getMessage();
    	}
		tm.addTest("User name correct", test1, testStack1);
		
		//Remove the test object
		pm.deletePersistent(test);
		
    	return tm.getJSONObject();
    }
	
	/**
	 * Tests the checkUserName method
	 * @return Test results JSON
	 */
	private static JSONObject testCheckUserName() {
		TestMethod tm = new TestMethod("testCheckUserName");
		
		//Test base
		String viewerId = "abcd";
		String email = "efgh";
		String authDomain = "ijkl";
		String userId = "mnop";
		String userName = "qrst";
		User user = new User(email, authDomain, userId);
		TUser test = getTUserByViewerId(viewerId);
		saveUserName(user, userName);
    	
		//Test 1
		Boolean test1 = false;
    	String testStack1 = "";
    	try {
    		test1 = (checkUserName(userName));
    	} catch (Exception e) {
    		testStack1 = e.getMessage();
    	}
		tm.addTest("User name exists", test1, testStack1);
		
		//Test 2
		Boolean test2 = false;
    	String testStack2 = "";
    	try {
    		test2 = (checkUserName("a1b2"));
    	} catch (Exception e) {
    		testStack2 = e.getMessage();
    	}
		tm.addTest("User name does not exist", test2, testStack2);
		
		//Remove the test object
		pm.deletePersistent(test);
		
    	return tm.getJSONObject();
    }
	
	/**
	 * Tests the getUserNameSuggestions method
	 * @return Test results JSON
	 */
	private static JSONObject testGetUserNameSuggestions() {
		TestMethod tm = new TestMethod("testGetUserNameSuggestions");
		
		//Test base
		String userName = "qrst";
		String[] test = getUserNameSuggestions(userName);
    	
		//Test 1
		Boolean test1 = false;
    	String testStack1 = "";
    	try {
    		test1 = (test.length == 3);
    	} catch (Exception e) {
    		testStack1 = e.getMessage();
    	}
		tm.addTest("Length correct", test1, testStack1);
		
    	return tm.getJSONObject();
    }

	
}
